#!/usr/bin/env python 
"""

atramentum.py
Version: 0.1.0
Copyright 2009 by Mitch Amiano, mitch.amiano@agilemarkup.com

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

"""
import os
import sys
import re
import subprocess
from xml.etree import ElementTree

import inkex
import simplestyle

import logging
logging.basicConfig(filename="Atramentum.log", level=logging.DEBUG)

class atramentum(inkex.Effect):
    """Atramentum builds outputs using annotated SVG as the specification."""
    def __init__(self):
        inkex.Effect.__init__(self)
        
        self.OptionParser.add_option("-z", "--tab",
                                     action="store", type="string", 
                                     dest="tabs", default="",
                                     help="Notebook tabs")
        self.OptionParser.add_option("-d", "--directory",
                                     action="store", type="string", 
                                     dest="directory", default=os.path.expanduser("~"),
                                     help="Destination directory")
        self.OptionParser.add_option("-b", "--basename",
                                     action="store", type="string",
                                     dest="basename", default="index",
                                     help="Name prefix for html and css file(s)")
        self.OptionParser.add_option("-p", "--protectfiles",
                                     action="store", type="inkbool",
                                     dest="protectfiles", default=True,
                                     help="Protect existing files from being changed.")
                                     
        self.OptionParser.add_option("-n", "--imagedpi",
                                     action="store", type="int",
                                     dest="exportdpi", default=129,
                                     help="Export images at this dots per inch.")
        self.OptionParser.add_option("-g", "--gopngs",
                                     action="store", type="inkbool",
                                     dest="gopngs", default=False,
                                     help="Export objects w/ id='..._img...' as PNG files.")
        self.OptionParser.add_option("--gosplinter",
                                     action="store", type="inkbool",
                                     dest="gosplinter", default=False,
                                     help="Splinter widgets to their own svg files.")
                                     
        self.OptionParser.add_option("--gojquery",
                                     action="store", type="inkbool",
                                     dest="gojquery", default=False,
                                     help="Include JQuery hooks and CSS classes.")                                     
        self.OptionParser.add_option("--golinks",
                                     action="store", type="inkbool",
                                     dest="golinks", default=False,
                                     help="Make widgets link to specified href.")
                                     
        self.OptionParser.add_option("-v", "--verboselogging",
                                     action="store", type="inkbool",
                                     dest="debug", default=False,
                                     help="Verbosely record script output to a log file.")
        self.OptionParser.add_option("-x", "--xsltmodulepath",
                                     action="store", type="string",
                                     dest="xsltmodulepath", default="C:/Program Files/Inkscape/share/extensions/atramentum.xsl",
                                     help="Render using the specified XSLT stylesheet.")

        
    def dbg(self,msg):
        if self.options.debug:
           logging.debug(msg)

    def effect(self):
        """
        Core behavior: iterate over the Inkscape layers and generate images, 
        then invoke an XSLT process to generate an XHTML interface and a CSS stylesheet. 
        
        self.args[-1] contains the path to a copy of the working SVG document in Inkscape.
        Modifying that path will have the effect of changing the working document.
        Inkex will then read it to return SVG to Inkscape upon completing the effect. 

        Inkscape's extension mechanism uses the stdout to read a new copy of the working file. 
        Therefore: DO NOT write anything to output unintentionally. 
        """
        self.dbg("Atramentum.effect started")
        temp_filepath = self.args[-1]

        # Invoke an XSLT Transform to construct an XHTML and CSS shell
        self.composexhtml()

        imageRoot = self.document.getroot()
        
        #~ logging.debug(temp_filepath)
        #~ logging.debug(self.document)
        #~ logging.debug(ElementTree.tostring(imageRoot))
        #~ logging.debug(self.options.layer)
        #~ logging.debug(ElementTree.tostring(self.document.getroot()))
        
        # Turn display to none for every layer by default, so only the content in one layer shows at a time
        layers = [ lyr for lyr in imageRoot.xpath( "/svg:svg/svg:g[ @i:groupmode='layer' ]", namespaces={'svg':'http://www.w3.org/2000/svg', 'i':'http://www.inkscape.org/namespaces/inkscape'} ) ]
        originallayerstyles = []
        for lyr in layers:   # Save styles to restore them later
           originallayerstyles.append( self.getattribute(lyr, 'style'))
           lyrstyles = simplestyle.parseStyle(self.getattribute(lyr, 'style'))  # convert style attribute value to a dictionary
           lyrstyles['display'] = 'none'      # change the display property to hide the layers
           lyr.attrib['style'] = simplestyle.formatStyle(lyrstyles)     # replace the style attribute value

        # For each layer group, show it and then export its objects, then hide it again
        for lyr in layers:
           groupid = lyr.attrib['id']
           # lyr.xpath("@id")[0]
           grouplabel = lyr.attrib['{http://www.inkscape.org/namespaces/inkscape}label']
           # xpath("@i:label", namespaces={"i":"http://www.inkscape.org/namespaces/inkscape"} )[0]

           # Make this particular layer group visible 
           grpstyles = simplestyle.parseStyle(lyr.attrib['style'])
           grpstyles['display'] = 'inline'
           lyr.attrib['style'] = simplestyle.formatStyle(grpstyles)

           # Get a list of _img figures and backgrounds from the current layer ; 
           # We export groups w/ a rect in which the id attribute contains "_img", 
           # as well as other objects with an id containing "_img"
           # Similarly a layer containing a rect with id having _bgimg is exported as a background,
           # (a layer containing a rect with id containing _bgcolor is taken as a CSS background color.)
           figures = []
           groupedfigures = []
           figurestyles = []
           groupedfigurestyles = []
           # Get the backgrounds first. We want to export the background and hide it so that other 
	   # exports are not affected by the background.
           for fig in lyr.xpath( "svg:rect[contains(@id, '_bgimg') or contains(@id,'_bgcolor')]", namespaces={'svg':'http://www.w3.org/2000/svg'}):
               figures.append(fig)
               figurestyles.append(self.getattribute(fig, 'style')) # copy the existing style
               figstyles = simplestyle.parseStyle(self.getattribute(fig, 'style'))  # convert style attribute value to a dictionary
               figstyles['display'] = 'none'      # change the display property to hide the object
               fig.attrib['style'] = simplestyle.formatStyle(figstyles)     # replace the style attribute value

               # Update the temp SVG working file with the export styles we've just updated
               self.document.write(temp_filepath)

           # If the user wants to export figures, objects with "_img" in the @id, stuff their styles away too
           if self.options.gopngs:

              # Hide figures created as simple SVG elements with "@id"s containing "_img" 
              for fig in lyr.xpath( "svg:*[contains(@id,'_img')]", namespaces={'svg':'http://www.w3.org/2000/svg'}):
                figures.append(fig)
                figurestyles.append( self.getattribute(fig, 'style')) # copy the existing style
		# Hide the figure by default; each will be set to display as it is exported.
		# Note this hides both any bounding rectangle inside of a groups as well as ungrouped figures 
                figstyles = simplestyle.parseStyle(self.getattribute(fig,'style'))
                figstyles['display'] = 'none'
                fig.attrib['style'] = simplestyle.formatStyle(figstyles)

              # Hide figures created as a group with a bounding rectangle.
              # The group must be hidden so that overlapping objects do not affect one anothers' exported images
	      # Note this has no affect upon objects which are not marked as figures with "_img" in the id
	      for groupedfig in lyr.xpath( "svg:g[svg:rect[contains(@id,'_img')]]", namespaces={'svg':'http://www.w3.org/2000/svg'}):
                groupedfigures.append(groupedfig)
                groupedfigurestyles.append( self.getattribute(groupedfig, 'style')) # copy the existing style
		# hide each group figure by default; it will be set visible again as it is exported
                figstyles = simplestyle.parseStyle(self.getattribute(groupedfig,'style'))
                figstyles['display'] = 'none'
                groupedfig.attrib['style'] = simplestyle.formatStyle(figstyles)

           # Log the number of figures and backgrounds exported
           self.dbg("Atramentum.effect: %d figures or backgrounds detected for layer %s " % (len(figures) , grouplabel) )
           
           # Update the temp SVG working file with the export styles we've just updated
           self.document.write(temp_filepath)
               
           # Use Inkscape to export each ungrouped figure now, using the id prefix (anything before the first "_") as the file name.
           # We don't use the label attribute because labels can have spaces and special characters; 
           # spaces have to be re-coded as %20 in URLs and most special characters are not valid filename characters. 
           for fig in figures:
               fig_id = fig.attrib['id']
               id_prefix = fig.attrib['id'][:fig.attrib['id'].find("_")]
               filename = id_prefix + ".png" 

	       # unhide the figure 
               figstyles = simplestyle.parseStyle(self.getattribute(fig,'style'))
               figstyles['display'] = 'inline'
               fig.attrib['style'] = simplestyle.formatStyle(figstyles)
               # Update the temp SVG working file with the export styles we've just updated
               self.document.write(temp_filepath)
               
               exportfile = os.path.join(os.path.abspath(self.options.directory), filename)
               
               if os.path.exists(exportfile) and self.options.protectfiles:
                   sys.stderr.write('File \'%s\' exists already. Protect Files option is set to true. Not overwritten.\n' % exportfile)
                   self.dbg("Atramentum.effect: refused to overwrite %s " % exportfile )
                   continue

               self.dbg("Atramentum.effect: Invoking pipe for %s " % exportfile )
               # Invoke Inkscape to export a PNG for this figure
               # Non-zero blur filters increase the visible size of a object and clipping when rasterized on export.
               commandlineargs = ["inkscape", "-d",  "%d" % self.options.exportdpi, "-i", fig_id, "-e", exportfile, temp_filepath]
               self.dbg("Atramentum.effect: %s " % ' '.join(commandlineargs) )
               
               p = subprocess.Popen(commandlineargs, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
               stdout = p.communicate()[0]   # Fetch pipe output and pass it along
               exitcode = p.wait() # block until thread completes
               
               self.dbg("Atramentum.effect: Command output (%s): %s " % (exitcode, stdout))
               
	       # hide the figure yet again so as not to interfere with the next figure exported
               figstyles = simplestyle.parseStyle(self.getattribute(fig,'style'))
               figstyles['display'] = 'none'
               fig.attrib['style'] = simplestyle.formatStyle(figstyles)
               
           # Use Inkscape to export each grouped figure now, using the rect id prefix as the file name.
           for fig in groupedfigures:
	       bbox = fig.xpath( "svg:rect[contains(@id,'_img')]", namespaces={'svg':'http://www.w3.org/2000/svg'})[0]

               fig_id = bbox.attrib['id']
               id_prefix = bbox.attrib['id'][:bbox.attrib['id'].find("_")]
               filename = id_prefix + ".png" 

	       # unhide the figure 
               figstyles = simplestyle.parseStyle(self.getattribute(fig,'style'))
               figstyles['display'] = 'inline'
               fig.attrib['style'] = simplestyle.formatStyle(figstyles)
	       # LEFTOFF. the rest of this is the same as the part in the previous loop. S/be refactored.
               # Update the temp SVG working file with the export styles we've just updated
               self.document.write(temp_filepath)
               
               exportfile = os.path.join(os.path.abspath(self.options.directory), filename)
               
               if os.path.exists(exportfile) and self.options.protectfiles:
                   sys.stderr.write('File \'%s\' exists already. Protect Files option is set to true. Not overwritten.\n' % exportfile)
                   self.dbg("Atramentum.effect: refused to overwrite %s " % exportfile )
                   continue

               self.dbg("Atramentum.effect: Invoking pipe for %s " % exportfile )
               # Invoke Inkscape to export a PNG for this figure
               # Using hints allows the user to manually configure by exporting a bitmap one time
               # The user is not expected to make the bounding rectangle sized to fit the visible content.
               # Non-zero blur filters increase the visible size of a object and clipping when rasterized on export.
               # So some rectangles may need to be slightly oversized.
               commandlineargs = ["inkscape", "-d",  "%d" % self.options.exportdpi, "-i", fig_id, "-e", exportfile, temp_filepath]
               self.dbg("Atramentum.effect: %s " % ' '.join(commandlineargs) )
               
               p = subprocess.Popen(commandlineargs, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
               stdout = p.communicate()[0]   # Fetch pipe output and pass it along
               exitcode = p.wait() # block until thread completes
               
               self.dbg("Atramentum.effect: Command output (%s): %s " % (exitcode, stdout))
               
	       # hide the figure yet again so as not to interfere with the next figure exported
               figstyles = simplestyle.parseStyle(self.getattribute(fig,'style'))
               figstyles['display'] = 'none'
               fig.attrib['style'] = simplestyle.formatStyle(figstyles)

           # Restore the visibilities of the figures and grouped figures
	   # (They'll still be hidden if the parent layer is hidden.)
           for fig, figstyle in zip(figures, figurestyles):
               fig.attrib['style'] = figstyle
           for gfig, gfigstyle in zip(groupedfigures, groupedfigurestyles):
               gfig.attrib['style'] = gfigstyle

           # Make the current layer invisible again to avoid altering any other layer's exported rasters
           grpstyles = simplestyle.parseStyle(lyr.attrib['style'])
           grpstyles['display'] = 'none'
           lyr.attrib['style'] = simplestyle.formatStyle(grpstyles)

           # This ends the loop over each layer group

	# Restore the visiblity of the layer groups
        for layer, layerstyle in zip(layers, originallayerstyles):
           lyr.attrib['style'] = layerstyle
           
        self.document.write(temp_filepath)
        self.dbg("Atramentum.effect ended")
        
        #sys.stdout.write('All done')
        #sys.exit(0)
    
    def getattribute(self,elem,attname):   # ignore key errors when attribute not found
       try:
          return elem.attrib[attname]
       except KeyError, ker:
          return ""
                   
    def composexhtml(self):
        self.dbg("Atramentum.composexhtml started")
        # Fetch the XSLT transformation module
        xslttree = inkex.etree.parse( self.options.xsltmodulepath )
        # self.dbg( inkex.etree.tostring(xslttree.getroot()) )
        xsltmodule = inkex.etree.XSLT( xslttree )
        # Note: The module should use xsltproc's document() extension
        self.dbg( "pngs: %s, splinter: %s, jquery: %s, links: %s" % (self.options.gopngs, self.options.gosplinter, self.options.gojquery, self.options.golinks ) )
        tf={True: "true()",False: "false()"}
        # use a hash to lookup (ternary op not in Python)
        transformresult = xsltmodule( self.document, base_dir="'%s'" % self.options.directory, 
            gopngs="%s" % ( tf[self.options.gopngs] ),
            gosplinter="%s" % ( tf[self.options.gosplinter] ),
            gojquery="%s" % ( tf[self.options.gojquery] ),
            golinks="%s" % ( tf[self.options.golinks] )
            ) 
        # we will mostly ignore the standard output of the transform.
        self.dbg( str(transformresult) )
        self.dbg("Atramentum.composexhtml ended")
        


def _main():
    e = atramentum()
    e.affect()
    
if __name__ == "__main__":
   _main()

